﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Data;
using RAD.LightOne.Data;
using System.Diagnostics;
using System.Reflection;
using System.Reflection.Emit;

using Dclass = RAD.LightOne.DynamicClassAttribute; //definição de alias para ´reduzir e facilitar o entendimento.

namespace RAD.LightOne
{
    #region Documentation
    /// <summary>
    /// Classe reponsável pela transaformação de resultados de consultas em objetos criados dinâmicamente.
    /// </summary>
    #endregion
    [DataObject]
    public static class DynamicDataFactory
    {
        #region Fields

        const string FIELD_PREFIX = "m_";
        const string GET_PREFIX = "get_";

        private static Dictionary<string, Type> dynamicTypes;

        #endregion

        #region Constructors

        static DynamicDataFactory()
        {
            dynamicTypes = new Dictionary<string, Type>();
        }


        #endregion

        #region Methods

        public static IEnumerable<T> Select<T>(string sql, params object[] args) where T: MyObject
        {


            return null;
        }

        #region Documentation
        /// <summary>
        /// Executa a consulta passada no parâmetro, segundo os argumentos desejados.
        /// </summary>
        /// <returns>
        /// <see cref="IEnumerable"/> de tipo dinâmico que representa a resposta à consulta 
        /// </returns>
        #endregion
        [DataObjectMethod(DataObjectMethodType.Select)]
        public static IEnumerable<dynamic> Select(string sql, params object[] args)
        {
            var dbe = DBEngine.GetInstance();
            DataTable data = new DataTable();
            DataTable schema = new DataTable();
            StackTrace trace = new StackTrace();
            dbe.Fill(data, schema, sql, args);
            Type dynamicType = BuildDynamicType(schema, trace);
            return BuildResponse(dynamicType, data);
        }

        #region Documentation
        /// <summary>
        /// Constroi e registra o tipo dinâmico segundo a chamada.
        /// </summary>
        #endregion
        private static Type BuildDynamicType(DataTable schema, StackTrace trace)
        {
            var attrib = trace.GetFrames().Where(f => Dclass.IsDefined(f.GetMethod())).Select<StackFrame, Dclass>(f => Dclass.GetDefinedAttribute(f.GetMethod())).LastOrDefault();

            const string ASSEMBLY_FORMAT = "{0}.Dynamics";
            const string TYPE_FORMAT = "{0}.{1}.{2}By";
            var callerFrame = trace.GetFrame(1);

            string assemblyName;
            string typeName;

            if (attrib != null && !string.IsNullOrWhiteSpace(attrib.Namespace))
            {
                assemblyName = string.Format(ASSEMBLY_FORMAT, attrib.Namespace);
            }
            else
            {
                assemblyName = string.Format(ASSEMBLY_FORMAT, callerFrame.GetMethod().DeclaringType.Assembly.GetName().Name);
            }
            if (attrib != null)
            {
                typeName = attrib.ClassName;
            }
            else
            {
                var callerMethod = callerFrame.GetMethod();
                StringBuilder typeNameBuilder = new StringBuilder();
                typeNameBuilder.AppendFormat(TYPE_FORMAT, assemblyName, callerMethod.DeclaringType.Name, callerMethod.Name);
                if (callerMethod.GetParameters().Length > 0)
                {
                    foreach (var a in callerMethod.GetParameters())
                    {
                        var name = a.Name.ToArray();
                        name[0] = Char.ToUpper(name[0]);
                        typeNameBuilder.Append(new string(name));
                    }
                }
                else
                {
                    typeNameBuilder.Append("Void");
                }
                typeName = typeNameBuilder.ToString();
            }

            
            if (!dynamicTypes.ContainsKey(typeName))
            {
                AssemblyName aName = new AssemblyName(assemblyName);
                AssemblyBuilder ab = AppDomain.CurrentDomain.DefineDynamicAssembly(aName, AssemblyBuilderAccess.Run);
                ModuleBuilder mb = ab.DefineDynamicModule(aName.Name);
                TypeBuilder tb = mb.DefineType(typeName, TypeAttributes.Class | TypeAttributes.Public | TypeAttributes.Serializable | TypeAttributes.UnicodeClass | TypeAttributes.Sealed | TypeAttributes.AutoLayout);
                var cb = tb.DefineConstructor(MethodAttributes.Public , CallingConventions.Standard, Type.EmptyTypes);
                //criando os campos
                List<FieldBuilder> fields = new List<FieldBuilder>();
                foreach (var col in schema.Columns.Cast<DataColumn>())
                {
                    Type targetType = (col.AllowDBNull && col.DataType != typeof(string)) ? typeof(Nullable<>).MakeGenericType(col.DataType) : col.DataType;
                    fields.Add(tb.DefineField( FIELD_PREFIX + col.ColumnName, targetType, FieldAttributes.Private));
                }

                //criando as propriedades para os campos
                List<PropertyBuilder> properties = new List<PropertyBuilder>();
                foreach (var f in fields)
                {
                    properties.Add(tb.DefineProperty(f.Name.Remove(0, FIELD_PREFIX.Length), System.Reflection.PropertyAttributes.None, CallingConventions.Any, f.FieldType, null));
                }

                //definindo getters para as propriedades.
                MethodAttributes getAttrib = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig;
                foreach (var p in properties)
                {
                    var newMethod = tb.DefineMethod(GET_PREFIX + p.Name, getAttrib, p.PropertyType, Type.EmptyTypes);
                    ILGenerator ilGen = newMethod.GetILGenerator();
                    ilGen.Emit(OpCodes.Ldarg_0);
                    ilGen.Emit(OpCodes.Ldfld, fields.Where(s => s.Name == (FIELD_PREFIX + p.Name)).FirstOrDefault()); //selecionando o campo de referência.
                    ilGen.Emit(OpCodes.Ret);

                    //atrelando o método à propriedade.
                    p.SetGetMethod(newMethod);
                }
                ConstructorInfo objCtor = typeof(object).GetConstructor(Type.EmptyTypes);

                var ctorIL = cb.GetILGenerator();
                ctorIL.Emit(OpCodes.Ldarg_0);
                ctorIL.Emit(OpCodes.Call, objCtor);
                ctorIL.Emit(OpCodes.Ret);

                Type dType = tb.CreateType();

                dynamicTypes.Add(typeName, dType);
            }

            return dynamicTypes[typeName];
        }

        #region Documentation
        /// <summary>
        /// Transforma a resposta armazenada no <see cref="DataTable"/> em um enumerável de instâncias do tipo definido.
        /// </summary>
        #endregion
        private static IEnumerable<dynamic> BuildResponse(Type dynamicType, DataTable data)
        {
            List<dynamic> dynamics = new List<dynamic>();
            foreach (var row in data.Rows.Cast<DataRow>())
            {
                dynamic dyn = Activator.CreateInstance(dynamicType);
                foreach (var info in dynamicType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance))
                {
                    var fName = info.Name.Remove(0, FIELD_PREFIX.Length);
                    if (data.Columns.Contains(fName))
                    {
                        if (!(row[fName] is DBNull))
                        {
                            info.SetValue(dyn, row[fName]);
                        }
                        else if (info.FieldType == typeof(string))
                        {
                            info.SetValue(dyn, row[fName] is DBNull ? null : row[fName]);
                        }
                        else if (!(info.FieldType.UnderlyingSystemType.IsGenericType && (info.FieldType.UnderlyingSystemType.GetGenericTypeDefinition() == typeof(Nullable<>))))
                        {
                            const string message = "The field {0} on dynamic type {1} cannot be null.";
                            throw new ArgumentNullException(string.Format(message, fName, info.DeclaringType.Name));
                        }
                    }
                    else
                    {
                        throw new InvalidProgramException();
                    }
                }
                dynamics.Add(dyn);
            }
            return dynamics;
        }

        #endregion
    }
}
